<?php
/**
 * Zend Framework
 * LICENSE
 * This source file is subject to the new BSD license that is bundled
 * with this package in the file LICENSE.txt.
 * It is also available through the world-wide-web at this URL:
 * http://framework.zend.com/license/new-bsd
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@zend.com so we can send you a copy immediately.
 * @category Zend
 * @package Zend_Cache
 * @subpackage Zend_Cache_Frontend
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 * @version $Id: Page.php 24593 2012-01-05 20:35:02Z matthew $
 */
/**
 *
 * @see Zend_Cache_Core
 */
require_once 'Zend/Cache/Core.php';

/**
 *
 * @package Zend_Cache
 * @subpackage Zend_Cache_Frontend
 * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (http://www.zend.com)
 * @license http://framework.zend.com/license/new-bsd New BSD License
 */
class Zend_Cache_Frontend_Page extends Zend_Cache_Core {

    /**
     * This frontend specific options
     * ====> (boolean) http_conditional :
     * - if true, http conditional mode is on
     * WARNING : http_conditional OPTION IS NOT IMPLEMENTED FOR THE MOMENT (TODO)
     * ====> (boolean) debug_header :
     * - if true, a debug text is added before each cached pages
     * ====> (boolean) content_type_memorization :
     * - deprecated => use memorize_headers instead
     * - if the Content-Type header is sent after the cache was started, the
     * corresponding value can be memorized and replayed when the cache is hit
     * (if false (default), the frontend doesn't take care of Content-Type header)
     * ====> (array) memorize_headers :
     * - an array of strings corresponding to some HTTP headers name. Listed headers
     * will be stored with cache datas and "replayed" when the cache is hit
     * ====> (array) default_options :
     * - an associative array of default options :
     * - (boolean) cache : cache is on by default if true
     * - (boolean) cacheWithXXXVariables (XXXX = 'Get', 'Post', 'Session', 'Files' or 'Cookie') :
     * if true, cache is still on even if there are some variables in this superglobal array
     * if false, cache is off if there are some variables in this superglobal array
     * - (boolean) makeIdWithXXXVariables (XXXX = 'Get', 'Post', 'Session', 'Files' or 'Cookie') :
     * if true, we have to use the content of this superglobal array to make a cache id
     * if false, the cache id won't be dependent of the content of this superglobal array
     * - (int) specific_lifetime : cache specific lifetime
     * (false => global lifetime is used, null => infinite lifetime,
     * integer => this lifetime is used), this "lifetime" is probably only
     * usefull when used with "regexps" array
     * - (array) tags : array of tags (strings)
     * - (int) priority : integer between 0 (very low priority) and 10 (maximum priority) used by
     * some particular backends
     * ====> (array) regexps :
     * - an associative array to set options only for some REQUEST_URI
     * - keys are (pcre) regexps
     * - values are associative array with specific options to set if the regexp matchs on $_SERVER['REQUEST_URI']
     * (see default_options for the list of available options)
     * - if several regexps match the $_SERVER['REQUEST_URI'], only the last one will be used
     * @var array options
     */
    protected $_specificOptions = array('http_conditional' => false, 'debug_header' => false, 'content_type_memorization' => false, 'memorize_headers' => array(), 'default_options' => array('cache_with_get_variables' => false, 'cache_with_post_variables' => false, 'cache_with_session_variables' => false, 'cache_with_files_variables' => false, 'cache_with_cookie_variables' => false, 'make_id_with_get_variables' => true, 'make_id_with_post_variables' => true, 'make_id_with_session_variables' => true, 'make_id_with_files_variables' => true, 'make_id_with_cookie_variables' => true, 'cache' => true, 'specific_lifetime' => false, 'tags' => array(), 'priority' => null), 'regexps' => array());

    /**
     * Internal array to store some options
     * @var array associative array of options
     */
    protected $_activeOptions = array();

    /**
     * If true, the page won't be cached
     * @var boolean
     */
    protected $_cancel = false;

    /**
     * Constructor
     * @param array $options Associative array of options
     * @param boolean $doNotTestCacheValidity If set to true, the cache validity won't be tested
     * @throws Zend_Cache_Exception
     * @return void
     */
    public function __construct (array $options = array()) {
        while (list ($name, $value) = each ($options)) {
            $name = strtolower ($name);
            switch ($name) {
                case 'regexps' :
                    $this -> _setRegexps ($value);
                    break;
                case 'default_options' :
                    $this -> _setDefaultOptions ($value);
                    break;
                case 'content_type_memorization' :
                    $this -> _setContentTypeMemorization ($value);
                    break;
                default :
                    $this -> setOption ($name, $value);
            }
        }
        if (isset ($this -> _specificOptions['http_conditional'])) {
            if ($this -> _specificOptions['http_conditional']) {
                Zend_Cache::throwException ('http_conditional is not implemented for the moment !');
            }
        }
        $this -> setOption ('automatic_serialization', true);
    }

    /**
     * Specific setter for the 'default_options' option (with some additional tests)
     * @param array $options Associative array
     * @throws Zend_Cache_Exception
     * @return void
     */
    protected function _setDefaultOptions ($options) {
        if ( ! is_array ($options)) {
            Zend_Cache::throwException ('default_options must be an array !');
        }
        foreach ($options as $key => $value) {
            if ( ! is_string ($key)) {
                Zend_Cache::throwException ("invalid option [$key] !");
            }
            $key = strtolower ($key);
            if (isset ($this -> _specificOptions['default_options'][$key])) {
                $this -> _specificOptions['default_options'][$key] = $value;
            }
        }
    }

    /**
     * Set the deprecated contentTypeMemorization option
     * @param boolean $value value
     * @return void
     * @deprecated
     *
     *
     *
     *
     *
     */
    protected function _setContentTypeMemorization ($value) {
        $found = null;
        foreach ($this -> _specificOptions['memorize_headers'] as $key => $value) {
            if (strtolower ($value) == 'content-type') {
                $found = $key;
            }
        }
        if ($value) {
            if ( ! $found) {
                $this -> _specificOptions['memorize_headers'][] = 'Content-Type';
            }
        } else {
            if ($found) {
                unset ($this -> _specificOptions['memorize_headers'][$found]);
            }
        }
    }

    /**
     * Specific setter for the 'regexps' option (with some additional tests)
     * @param array $options Associative array
     * @throws Zend_Cache_Exception
     * @return void
     */
    protected function _setRegexps ($regexps) {
        if ( ! is_array ($regexps)) {
            Zend_Cache::throwException ('regexps option must be an array !');
        }
        foreach ($regexps as $regexp => $conf) {
            if ( ! is_array ($conf)) {
                Zend_Cache::throwException ('regexps option must be an array of arrays !');
            }
            $validKeys = array_keys ($this -> _specificOptions['default_options']);
            foreach ($conf as $key => $value) {
                if ( ! is_string ($key)) {
                    Zend_Cache::throwException ("unknown option [$key] !");
                }
                $key = strtolower ($key);
                if ( ! in_array ($key, $validKeys)) {
                    unset ($regexps[$regexp][$key]);
                }
            }
        }
        $this -> setOption ('regexps', $regexps);
    }

    /**
     * Start the cache
     * @param string $id (optional) A cache id (if you set a value here, maybe you have to use Output frontend instead)
     * @param boolean $doNotDie For unit testing only !
     * @return boolean True if the cache is hit (false else)
     */
    public function start ($id = false, $doNotDie = false) {
        $this -> _cancel = false;
        $lastMatchingRegexp = null;
        if (isset ($_SERVER['REQUEST_URI'])) {
            foreach ($this -> _specificOptions['regexps'] as $regexp => $conf) {
                if (preg_match ("`$regexp`", $_SERVER['REQUEST_URI'])) {
                    $lastMatchingRegexp = $regexp;
                }
            }
        }
        $this -> _activeOptions = $this -> _specificOptions['default_options'];
        if ($lastMatchingRegexp !== null) {
            $conf = $this -> _specificOptions['regexps'][$lastMatchingRegexp];
            foreach ($conf as $key => $value) {
                $this -> _activeOptions[$key] = $value;
            }
        }
        if ( ! ($this -> _activeOptions['cache'])) {
            return false;
        }
        if ( ! $id) {
            $id = $this -> _makeId ();
            if ( ! $id) {
                return false;
            }
        }
        $array = $this -> load ($id);
        if ($array !== false) {
            $data = $array['data'];
            $headers = $array['headers'];
            if ( ! headers_sent ()) {
                foreach ($headers as $key => $headerCouple) {
                    $name = $headerCouple[0];
                    $value = $headerCouple[1];
                    header ("$name: $value");
                }
            }
            if ($this -> _specificOptions['debug_header']) {
                echo 'DEBUG HEADER : This is a cached page !';
            }
            echo $data;
            if ($doNotDie) {
                return true;
            }
            die ();
        }
        ob_start (array($this, '_flush'));
        ob_implicit_flush (false);
        return false;
    }

    /**
     * Cancel the current caching process
     */
    public function cancel () {
        $this -> _cancel = true;
    }

    /**
     * callback for output buffering
     * (shouldn't really be called manually)
     * @param string $data Buffered output
     * @return string Data to send to browser
     */
    public function _flush ($data) {
        if ($this -> _cancel) {
            return $data;
        }
        $contentType = null;
        $storedHeaders = array();
        $headersList = headers_list ();
        foreach ($this -> _specificOptions['memorize_headers'] as $key => $headerName) {
            foreach ($headersList as $headerSent) {
                $tmp = explode (':', $headerSent);
                $headerSentName = trim (array_shift ($tmp));
                if (strtolower ($headerName) == strtolower ($headerSentName)) {
                    $headerSentValue = trim (implode (':', $tmp));
                    $storedHeaders[] = array($headerSentName, $headerSentValue);
                }
            }
        }
        $array = array('data' => $data, 'headers' => $storedHeaders);
        $this -> save ($array, null, $this -> _activeOptions['tags'], $this -> _activeOptions['specific_lifetime'], $this -> _activeOptions['priority']);
        return $data;
    }

    /**
     * Make an id depending on REQUEST_URI and superglobal arrays (depending on options)
     * @return mixed false cache id (string), false if the cache should have not to be used
     */
    protected function _makeId () {
        $tmp = $_SERVER['REQUEST_URI'];
        $array = explode ('?', $tmp, 2);
        $tmp = $array[0];
        foreach (array('Get', 'Post', 'Session', 'Files', 'Cookie') as $arrayName) {
            $tmp2 = $this -> _makePartialId ($arrayName, $this -> _activeOptions['cache_with_' . strtolower ($arrayName) . '_variables'], $this -> _activeOptions['make_id_with_' . strtolower ($arrayName) . '_variables']);
            if ($tmp2 === false) {
                return false;
            }
            $tmp = $tmp . $tmp2;
        }
        return md5 ($tmp);
    }

    /**
     * Make a partial id depending on options
     * @param string $arrayName Superglobal array name
     * @param bool $bool1 If true, cache is still on even if there are some variables in the superglobal array
     * @param bool $bool2 If true, we have to use the content of the superglobal array to make a partial id
     * @return mixed false id (string) or false if the cache should have not to be used
     */
    protected function _makePartialId ($arrayName, $bool1, $bool2) {
        switch ($arrayName) {
            case 'Get' :
                $var = $_GET;
                break;
            case 'Post' :
                $var = $_POST;
                break;
            case 'Session' :
                if (isset ($_SESSION)) {
                    $var = $_SESSION;
                } else {
                    $var = null;
                }
                break;
            case 'Cookie' :
                if (isset ($_COOKIE)) {
                    $var = $_COOKIE;
                } else {
                    $var = null;
                }
                break;
            case 'Files' :
                $var = $_FILES;
                break;
            default :
                return false;
        }
        if ($bool1) {
            if ($bool2) {
                return serialize ($var);
            }
            return '';
        }
        if (count ($var) > 0) {
            return false;
        }
        return '';
    }

}
